package nl.zoidberg.calculon.engine;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import nl.zoidberg.calculon.analyzer.GameScorer;
import nl.zoidberg.calculon.notation.PGNUtils;
import nl.zoidberg.calculon.opening.OpeningBook;

public class ChessEngine {
	private static final int DEPTH = 3;
	private static final Logger log = Logger.getLogger(ChessEngine.class.getName());
	private static int MAX_CACHE = Integer.parseInt(System.getProperty("calculon.maxcache", "250000"));
	
	private Map<String, Integer> cache = new HashMap<String, Integer>(MAX_CACHE);
	private GameScorer gameScorer;
	private long mSearch = 0, mHit = 0;
	private int searchDepth = DEPTH;
	
	public ChessEngine() {
		this.gameScorer = GameScorer.getDefaultScorer();
	}
	
	public ChessEngine(GameScorer gameScorer) {
		this.gameScorer = gameScorer;
	}
	
	public final int getDepth() {
		return searchDepth;
	}

	public final void setDepth(int depth) {
		this.searchDepth = depth;
	}

	public int getAlphaBetaScore(int depth, Board board) {
		int score = alphaBetaMax(Integer.MIN_VALUE, Integer.MAX_VALUE, depth, board);
		return score;
	}
	
	private int alphaBetaMax(int alpha, int beta, int depthLeft, Board board) {
		Iterator<Move> moveItr = new MoveGenerator(board);
		if(depthLeft == 0 || ! moveItr.hasNext()) {
			mSearch++;
			String id = board.getCacheId();
			Integer i = cache.get(id);
			if(i != null) {
				mHit++;
				return i.intValue();
			}
			int rv = gameScorer.score(board);
			if(rv == GameScorer.MATE_SCORE) {
				rv *= (depthLeft+1);
			}
			if(cache.size() < MAX_CACHE) {
				cache.put(id, rv);
			}
			return rv;
		}
		
		while(moveItr.hasNext()) {
			int score = alphaBetaMin(alpha, beta, depthLeft - 1, moveItr.next().getBoard());
			if(score >= beta) {
				return beta;
			}
			if(score > alpha) {
				alpha = score;
			}
		}
		
		return alpha;
	}

	private int alphaBetaMin(int alpha, int beta, int depthLeft, Board board) {
		Iterator<Move> moveItr = new MoveGenerator(board);
		if(depthLeft == 0 || ! moveItr.hasNext()) {
			mSearch++;
			String id = board.getCacheId();
			Integer i = cache.get(id);
			if(i != null) {
				mHit++;
				return -i.intValue();
			}
			int rv = gameScorer.score(board);
			if(rv == GameScorer.MATE_SCORE) {
				rv *= (depthLeft+1);
			}
			if(cache.size() < MAX_CACHE) {
				cache.put(id, rv);
			}
			return -rv;
		}

		while(moveItr.hasNext()) {
			int score = alphaBetaMax(alpha, beta, depthLeft - 1, moveItr.next().getBoard());
			if(score <= alpha) {
				return alpha;
			}
			if(score < beta) {
				beta = score;
			}
		}
		
		return beta;
	}
	
	public Map<String, Integer> getScoredMoves(Board board) {
		Map<String, Integer> rv = new HashMap<String, Integer>();

		for(Iterator<Move> moveItr = new MoveGenerator(board); moveItr.hasNext(); ) {
			Move move = moveItr.next();
			int score = getAlphaBetaScore(searchDepth, move.getBoard());
			rv.put(move.getMove(), score);
		}
		
		return rv;
	}
	
	public String getBestMove(Board board) {
		return selectBestMove(getScoredMoves(board));
	}

	public static String selectBestMove(Map<String, Integer> allMoves) {
		Map<String, Integer> bestMoves = selectBestMoves(allMoves);
		if(bestMoves.size() == 0) {
			return null;
		}
		List<String> moves = new ArrayList<String>(bestMoves.keySet());
		return moves.get((int) (Math.random() * moves.size()));
	}
	
	public String getPreferredMove(Board board) {
		String bookMove = OpeningBook.getDefaultBook() == null ? null : OpeningBook.getDefaultBook().getBookMove(board); 
		if(bookMove != null) {
			log.fine("Using book move: " + bookMove);
			return PGNUtils.toPgnMoveMap(board).get(bookMove);
		}

		Map<String, Integer> allMoves = ChessEngine.selectBestMoves(getScoredMoves(board));
		String rv = ChessEngine.selectBestMove(allMoves);
		return rv;
	}
	
	public static Map<String, Integer> selectBestMoves(Map<String, Integer> allMoves) {
		int bestScore = Integer.MAX_VALUE;
		Map<String, Integer> bestMoves = new HashMap<String, Integer>();
		for (String move : allMoves.keySet()) {
			int score = allMoves.get(move);
			if (score < bestScore) {
				bestMoves.clear();
				bestMoves.put(move, score);
				bestScore = score;
			} else if (score == bestScore) {
				bestMoves.put(move, score);
			}
		}
		
		return bestMoves;
	}
}
